一般我們從instance
取得attribute
或function
時,會先由instance.__dict__
找起,如果沒找到會再往上,順著生成instance
的class
的mro
順序繼續找。descriptor
是一種可以改變這種機制的有趣功能。雖然descriptor
大部份應用會著重在由instance
呼叫,這也會是我們接下來分享的重點,但是當其由class
或是super
呼叫時,各自有其細節要注意(註1
)。
descriptor
分為non-data descriptor
及data descriptor
。non-data descriptor
為一有實作__get__
的class
,而data descriptor
為一有實作__get__
加上__set__
或是__delete__
兩種其一的class
。
為方便稱呼,我將以desc_instance
來稱呼由descriptor
class
生成的instance
。
non-data descriptor
與data descriptor
。descriptor
實作方法。descriptor
實作方法。property
與Descriptor
。當我們想由instance
取得attribute
或function
時,如果遇到non-data descriptor
,會先確認該attribute
或function
是否存在於instance.__dict__
中,如果有的話會優先使用,如果沒有的話才會使用non-data descriptor
的__get__
。
__get__
的signature
如下:
__get__(self, instance, owner_cls)
self
是實作有__get__
的non-data descriptor
class
所生成的instance
,我們稱作desc_instance
。instance
是desc_instance
所在的class
所生成的instance
。owner_cls
是生成instance
的class
。乍看可能有點抽象,我們試著從# 01
的例子中來說明。
其中:
self
就是MyClass
中的non_data_desc
。instance
就是my_inst
。owner_cls
就是MyClass
。# 01
class NonDataDescriptor:
def __get__(self, instance, owner_cls):
print('NonDataDescriptor __get__ called')
class MyClass:
non_data_desc = NonDataDescriptor()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
print(f'{my_inst.non_data_desc=}') # None
my_inst.non_data_desc = 10 # shadow
print(f'{my_inst.non_data_desc=}') # 10
print(f'{my_inst.__dict__=}') # {'non_data_desc': 10}
print(f'{my_inst.non_data_desc=}') # 10
my_inst.__dict__={}
NonDataDescriptor __get__ called
my_inst.non_data_desc=None
my_inst.non_data_desc=10
my_inst.__dict__={'non_data_desc': 10}
my_inst.non_data_desc=10
my_inst
剛由MyClass
生成時,my_inst.__dict__
為一個空的dict
。my_inst.non_data_desc
來取值,由於my_inst.__dict__
找不到non_data_desc
,所以會繼續使用non_data_desc.__get__
來取值。而我們在__get__
中只有印出參數,所以回傳值為None
。my_inst.non_data_desc=10
來賦值,這相當於在my_inst.__dict__
中添加non_data_desc
為10。這可以由再次觀察my_inst.__dict__
來驗證。my_inst.non_data_desc
來取值,因為my_inst.__dict__
中已經有non_data_desc
,所以會回傳10,而不會呼叫non_data_desc.__get__
。當我們由instance
存取attribute
或function
時,如果遇到data descriptor
,會使用其實作的__get__
及__set__
(相當於shadow
instance.__dict__
)。
__set__
的signature
如下:
__set__(self, instance, value)
self
是實作有__get__
及__set__
的data descriptor
class
所生成的instance
,即desc_instance
本身。instance
是desc_instance
所在的class
所生成的instance
。value
是所傳入想指定的值。從# 02
的例子中來說明。
其中:
self
就是MyClass
中的data_desc
。instance
就是my_inst
。value
就是20
。# 02
class DataDescriptor:
def __get__(self, instance, owner_cls):
print('DataDescriptor __get__ called')
def __set__(self, instance, value):
print(f'DataDescriptor __set__ called, {value=}')
class MyClass:
data_desc = DataDescriptor()
if __name__ == '__main__':
my_inst = MyClass()
print(f'{my_inst.__dict__=}') # {}
my_inst.__dict__['data_desc'] = 10
print(f'{my_inst.data_desc=}') # None
my_inst.data_desc = 20 # always use data_desc.__set__
print(f'{my_inst.data_desc=}') # None
print(f'{my_inst.__dict__=}') # {}
my_inst.__dict__={}
DataDescriptor __get__ called
my_inst.data_desc=None
DataDescriptor __set__ called, value=20
DataDescriptor __get__ called
my_inst.data_desc=None
my_inst.__dict__={'data_desc': 10}
my_inst
剛由MyClass
生成時,my_inst.__dict__
為一個空的dict
。my_inst.__dict__
中手動插入data_desc
為10
(註2
)。my_inst.data_desc
來取值,由於data_desc
會shadow
instance.__dict__
,所以將會呼叫data_desc.__get__
。而我們在__get__
中只有印出參數,所以回傳值為None
。my_inst.data_desc=20
來賦值,這會呼叫data_desc.__set__
來進行賦值(但__set__
目前僅呼叫一次print
,並未實際賦值)。my_inst.data_desc
來取值,此語法仍會呼叫data_desc.__get__
,並回傳None
。此時如果再次驗證my_inst.__dict__
,會發現其中只有我們剛剛手動插入的data_desc
,其值依然為10
。non-data descriptor
class
生成的desc_instance
可能會被instance.__dict__
shadow
。data descriptor
class
生成的desc_instance
必定會shadow
instacne.__dict__
。註2:這邊我們必須使用這樣的語法,而不能使用my_inst.data_desc = 10
,因為這樣會呼叫data_desc.__set__
。
Descriptor
相關內容,大部份整理自python-deepdive-Part 4-Section 08-Descriptors、Descriptor HowTo Guide
及Python Morsels
練習題。